package rwt.server.mtl.java;

import java.io.UnsupportedEncodingException;
import java.util.*;

import rwt.server.RailroadMessages.*;

/**
 * A pure Java port of the original Message Translation Library written in Ada. Used when the
 * "MessageTranslationLibrary" property of the config is set to "Java".
 * <p>
 * <b>Comparison to Ada-native Message Translation Library.</b>
 * <p>
 * Advantages of Java MTL:
 * <ul>
 * <li>Faster because the Java MTL does not make native calls through JNI.</li>
 * <li>Does not require installation of GNAT 2011 and AJIS 2011.</li>
 * </ul>
 * <p>
 * Disadvantages of Java MTL:
 * <ul>
 * <li>While thorough unit-testing has been performed to ensure the correctness of Java MTL, the Ada MTL is
 * much older and more proven. The Ada MTL is compiled from the same source shared by the Admin Throttle,
 * Controller, and other existing Railroad programs.</li>
 * </ul>
 */
@SuppressWarnings("unused")
public class MessageTranslationLibrary
{
	public static final class SwitchStateType
	{
		public final static int Closed = 0;
		public final static int Thrown = 1;
		public final static int BeginClosed = 2;
		public final static int BeginThrown = 3;
		public final static int Read = 4;
		public final static int Unknown = 5;
	}
	
	public static final class DirectionType
	{
		public final static int Forward = 0;
		public final static int Backward = 1;
	}
	
	public static final class OnOffType
	{
		public final static int Off = 0;
		public final static int On = 1;
	}
	
	public static final class SectionStateType
	{
		public final static int free = 0;
		public final static int reserved = 1;
		public final static int occupied = 2;
		public final static int blocked = 3;
	}
	
	public static final class TrainStateType
	{
		public final static int moving = 0;
		public final static int waiting = 1;
		public final static int halted = 2;
		public final static int error = 3;
		public final static int beginChangeDirection = 4;
		public final static int beginWaiting = 5;
		public final static int beginHalted = 6;
	}
	
	public static final class SensorStateType
	{
		public final static int open = 0;
		public final static int closed = 1;
	}
	
	private static class BytePair
	{
		public byte lo;
		public byte hi;
		
		public BytePair(byte lo, byte hi)
		{
			this.lo = lo;
			this.hi = hi;
		}
	}
	
	private static final byte OPC_GPOFF = (byte) 0x82; // power OnOffType.Off
	private static final byte OPC_GPON = (byte) 0x83; // power OnOffType.On
	private static final byte OPC_LOCO_SPD = (byte) 0xA0; // set speed
	private static final byte OPC_LOCO_DIRF = (byte) 0xA1; // set direction, horn, bell, lights
	private static final byte OPC_LOCO_SND = (byte) 0xA2; // set mute and unmute sound
	private static final byte OPC_SW_REQ = (byte) 0xB0; // move a turnout
	private static final byte OPC_SW_REP = (byte) 0xB1; // report turnout now open/SwitchStateType.Thrown
	private static final byte OPC_SW_STATE = (byte) 0xBC; // request state of a turnout
	private static final byte OPC_INPUT_REP = (byte) 0xB2; // report sensor fired
	private static final byte OPC_LONG_ACK = (byte) 0xB4; // if 2nd byte = 3F, then insufficient slots
	private static final byte OPC_MOVE_SLOTS = (byte) 0xBA; // set a slot to in-use
	private static final byte OPC_LOCO_ADR = (byte) 0xBF; // request for slot data
	private static final byte OPC_SL_RD_DATA = (byte) 0xE7; // slot data response
	private static final byte OPC_WR_SL_DATA = (byte) 0xEF; // write data into a slot
	
	private static final int kBackward = 0x20; // 0010 0000
	private static final int kLightsOn = 0x10; // 0001 0000
	private static final int kHornOn = 0x02; // 0000 0010
	private static final int kBellOn = 0x01; // 0000 0001
	private static final int kMuteOn = 0x08; // 0000 1000 F8 OnOffType.On UT4
	private static final int kF5 = 0x01; // 0000 0001 F5 OnOffType.On UT4
	private static final int kF6 = 0x02; // 0000 0010 F6 OnOffType.On UT4
	
	private static final byte kReportClosed = (byte) 0x30;
	private static final byte kReportThrown = (byte) 0x20;
	private static final byte kRequestClose = (byte) 0x30;
	private static final byte kRequestThrow = (byte) 0x10;
	
	private static final byte putTrainState = (byte) 1;
	private static final byte putTrainPosition = (byte) 2;
	private static final byte putSectionState = (byte) 3;
	private static final byte putSwitchState = (byte) 4;
	private static final byte putSensorState = (byte) 5;
	private static final byte getSwitchSuccessor = (byte) 6;
	private static final byte putSwitchSuccessor = (byte) 7;
	private static final byte doLocoInit = (byte) 8;
	private static final byte putInitOutcome = (byte) 9;
	private static final byte doReadLayout = (byte) 10;
	private static final byte putReadLayoutResponse = (byte) 11;
	private static final byte doSaveState = (byte) 12;
	private static final byte putSaveResponse = (byte) 13;
	private static final byte doRestoreState = (byte) 14;
	private static final byte putRestoreResponse = (byte) 15;
	private static final byte msgTryToMoveAgain = (byte) 16;
	private static final byte msgFrontSensorFired = (byte) 17;
	private static final byte msgBackSensorFired = (byte) 18;
	private static final byte msgSensorError = (byte) 19;
	private static final byte msgLoseReservation = (byte) 20;
	private static final byte putTrainInformation = (byte) 21;
	private static final byte getSwitchStates = (byte) 22;
	private static final byte getFirstSwitch = (byte) 23;
	private static final byte msgTrainTaskQuit = (byte) 24;
	private static final byte msgReinitializeTrain = (byte) 25;
	private static final byte putPowerChangeComplete = (byte) 26;
	
	public static byte[] makePowerOnMsg(PowerOnMsg inMsg)
	{
		final int SIZE = 2;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_GPON;
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makePowerOffMsg(PowerOffMsg inMsg)
	{
		final int SIZE = 2;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_GPOFF;
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeMoveSlotsMsg(MoveSlotsMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_MOVE_SLOTS;
		message[1] = (byte) (inMsg.slot1);
		message[2] = (byte) (inMsg.slot2);
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeSwRepMsg(SwRepMsg inMsg)
	{
		final int SIZE = 4;
		byte[] Cmd = new byte[SIZE];
		final int SwitchNum = inMsg.switchId - 1;
		
		Cmd[0] = OPC_SW_REP;
		Cmd[1] = (byte) ((SwitchNum % 128) & 0x7F);
		Cmd[2] = (byte) (((SwitchNum / 128) & 0x0F) | 0x10);
		
		switch (inMsg.state)
		{
		case SwitchStateType.Closed:
		case SwitchStateType.BeginClosed:
			Cmd[2] = (byte) ((byte) 0x20 | Cmd[2]);
			break;
		default:
		}
		
		makeChecksumByte(Cmd);
		return Cmd;
	}
	
	public static byte[] makeLocoAdrMsg(LocoAdrMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_LOCO_ADR;
		message[2] = (byte) (inMsg.locoAddress % 128);
		message[1] = (byte) (inMsg.locoAddress / 128);
		
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeLocoDirfMsg(LocoDirfMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		int dirf = 0;
		
		message[0] = OPC_LOCO_DIRF;
		message[1] = (byte) inMsg.slot;
		
		if (inMsg.direction == DirectionType.Backward)
			dirf = dirf | kBackward;
		if (inMsg.light == OnOffType.On)
			dirf = dirf | kLightsOn;
		if (inMsg.horn == OnOffType.On)
			dirf = dirf | kHornOn;
		if (inMsg.bell == OnOffType.On)
			dirf = dirf | kBellOn;
		message[2] = (byte) dirf;
		
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeLocoSndMsg(LocoSndMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		int snd = 0;
		
		message[0] = OPC_LOCO_SND;
		message[1] = (byte) inMsg.slot;
		
		if (inMsg.mute == OnOffType.On)
			snd = snd | kMuteOn;
		if (inMsg.F5 == OnOffType.On)
			snd = snd | kF5;
		if (inMsg.F6 == OnOffType.On)
			snd = snd | kF6;
		message[2] = (byte) snd;
		
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeLocoSpdMsg(LocoSpdMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_LOCO_SPD;
		message[1] = (byte) inMsg.slot;
		message[2] = (byte) inMsg.speed;
		
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makeSwReqMsg(SwReqMsg inMsg)
	{
		final int SIZE = 4;
		byte[] message = new byte[SIZE];
		
		message[0] = OPC_SW_REQ;
		message[1] = (byte) (inMsg.switchId - 1);
		if (inMsg.direction == SwitchStateType.Thrown)
			message[2] = kRequestThrow;
		else
			message[2] = kRequestClose;
		
		makeChecksumByte(message);
		return message;
	}
	
	public static byte[] makePutTrainStateMsg(PutTrainStateMsg inMsg)
	{
		final int SIZE = 4;
		byte[] msg = new byte[SIZE];
		
		msg[0] = (byte) 0x00;
		msg[1] = putTrainState;
		msg[2] = (byte) inMsg.slot;
		msg[3] = (byte) inMsg.state;
		return msg;
	}
	
	public static byte[] makePutSwitchStateMsg(PutSwitchStateMsg inMsg)
	{
		final int SIZE = 4;
		byte[] msg = new byte[SIZE];
		
		msg[0] = 0;
		msg[1] = putSwitchState;
		msg[2] = (byte) inMsg.switchId;
		msg[3] = (byte) inMsg.state;
		return msg;
	}
	
	public static byte[] makeDoLocoInitMsg(DoLocoInitMsg inMsg)
	{
		List<Integer> sensors = inMsg.sensors;
		int sensorCount = ((sensors == null) ? 0 : sensors.size());
		final int SIZE = 5 + 2 * sensorCount;
		byte lowByte;
		byte highByte;
		
		byte[] msg = new byte[SIZE];
		
		msg[0] = (byte) 0x00;
		msg[1] = doLocoInit;
		BytePair bytes = convertNaturalToBytes(inMsg.locoAddress);
		msg[2] = bytes.lo;
		msg[3] = bytes.hi;
		if (inMsg.locoAddress == 9999)
			sensorCount = 0;
		
		msg[4] = (byte) sensorCount;
		for (int i = 1; i <= sensorCount; i++)
		{
			int value = sensors.get(i - 1);
			bytes = convertNaturalToBytes(value);
			msg[4 + (2 * i) - 1] = bytes.lo;
			msg[4 + (2 * i)] = bytes.hi;
		}
		
		return msg;
	}
	
	public static byte[] makePutInitOutcomeMsg(PutInitOutcomeMsg inMsg)
	{
		final int SIZE = 8;
		byte[] msg = new byte[SIZE];
		
		msg[0] = (byte) 0x00;
		msg[1] = putInitOutcome;
		
		BytePair bytes = convertNaturalToBytes(inMsg.physicalLocoAddress);
		msg[2] = bytes.lo;
		msg[3] = bytes.hi;
		msg[4] = (byte) (inMsg.physicalSlotNum);
		
		bytes = convertNaturalToBytes(inMsg.virtualLocoAddress);
		msg[5] = bytes.lo;
		msg[6] = bytes.hi;
		msg[7] = (byte) (inMsg.virtualSlotNum);
		return msg;
	}
	
	public static byte[] makeDoReadLayoutMsg(DoReadLayoutMsg inMsg)
	{
		byte[] fileNameBytes = null;
		try
		{
			fileNameBytes = inMsg.fileName.getBytes("UTF-8");
		}
		catch (UnsupportedEncodingException e)
		{
			/* UTF-8 always supported */
		}
		
		final int LENGTH = fileNameBytes.length;
		final int SIZE = 3 + LENGTH;
		byte[] msg = new byte[SIZE];
		
		msg[0] = (byte) 0x00;
		msg[1] = doReadLayout;
		msg[2] = (byte) (LENGTH);
		for (int i = 0; i < LENGTH; i++)
			msg[3 + i] = fileNameBytes[i];
		
		return msg;
	}
	
	public static byte[] makePutReadLayoutResponseMsg(PutReadLayoutResponseMsg inMsg)
	{
		final int SIZE = 5;
		byte[] msg = new byte[SIZE];
		
		msg[0] = 0;
		msg[1] = putReadLayoutResponse;
		msg[2] = (byte) inMsg.responseFlag;
		
		BytePair bytes = convertNaturalToBytes(inMsg.code);
		msg[3] = bytes.lo;
		msg[4] = bytes.hi;
		return msg;
	}
	
	public static byte[] makePutTrainInformationMsg(PutTrainInformationMsg inMsg)
	{
		final int SIZE = 9;
		byte[] msg = new byte[SIZE];
		
		msg[0] = 0;
		msg[1] = putTrainInformation;
		msg[2] = (byte) inMsg.slot;
		msg[3] = (byte) inMsg.speed;
		msg[4] = (byte) inMsg.direction;
		msg[5] = (byte) inMsg.light;
		msg[6] = (byte) inMsg.bell;
		msg[7] = (byte) inMsg.horn;
		msg[8] = (byte) inMsg.mute;
		return msg;
	}
	
	public static byte[] makeGetSwitchStatesMsg(GetSwitchStatesMsg inMsg)
	{
		final int SIZE = 2;
		byte[] msg = new byte[SIZE];
		
		msg[0] = 0;
		msg[1] = getSwitchStates;
		return msg;
	}
	
	public static byte[] makeSlRdDataMsg(SlRdDataMsg inMsg)
	{
		final int SIZE = 14;
		byte[] cmd = new byte[SIZE];
		
		byte addrHigh = (byte) (inMsg.locoAddress / 128);
		byte addrLow = (byte) (inMsg.locoAddress % 128);
		cmd[0] = OPC_SL_RD_DATA;
		cmd[1] = (byte) 0x0E;
		cmd[2] = (byte) (inMsg.slot);
		cmd[3] = (byte) 0x3B;
		cmd[4] = addrLow;
		cmd[5] = (byte) 0x00;
		cmd[6] = (byte) 0x00;
		cmd[7] = (byte) 0x00;
		cmd[8] = (byte) 0x00;
		cmd[9] = addrHigh;
		cmd[10] = (byte) 0x00;
		cmd[11] = (byte) 0x00;
		cmd[12] = (byte) 0x00;
		
		makeChecksumByte(cmd);
		return cmd;
	}
	
	public static byte[] makeWriteSlotDataToClearMsg(WriteSlotDataToClearMsg inMsg)
	{
		final int SIZE = 14;
		byte[] cmd = new byte[SIZE];
		
		byte addrHigh = 0;
		byte addrLow = 0;
		cmd[0] = OPC_WR_SL_DATA;
		cmd[1] = 0x0E;
		cmd[2] = (byte) inMsg.slotId;
		cmd[3] = 0x0B;
		cmd[4] = addrLow;
		cmd[5] = 0;
		cmd[6] = 0;
		cmd[7] = 0;
		cmd[8] = 0;
		cmd[9] = addrHigh;
		cmd[10] = 0;
		cmd[11] = 0;
		cmd[12] = 0;
		makeChecksumByte(cmd);
		return cmd;
	}
	
	public static WriteSlotDataToClearMsg splitWriteSlotDataToClearMsg(byte[] msg)
	{
		WriteSlotDataToClearMsg outMsg = new WriteSlotDataToClearMsg();
		outMsg.slotId = toInt(msg[2]);
		return outMsg;
	}
	
	public static SwRepMsg splitSwRepMsg(byte[] message)
	{
		SwRepMsg outMsg = new SwRepMsg();
		
		outMsg.switchId = 1 + (int) (message[1]) + (128 * (toInt(message[2]) & 0x0F));
		if ((message[2] & kReportClosed) == kReportClosed)
			outMsg.state = SwitchStateType.Closed;
		else
			outMsg.state = SwitchStateType.Thrown;
		
		return outMsg;
	}
	
	public static SwReqMsg splitSwReqMsg(byte[] message)
	{
		SwReqMsg outMsg = new SwReqMsg();
		
		outMsg.switchId = 1 + (int) (message[1]) + (128 * (toInt(message[2]) & 0x0F));
		if ((message[2] & kRequestClose) == kRequestClose)
			outMsg.direction = SwitchStateType.Closed;
		else
			outMsg.direction = SwitchStateType.Thrown;
		
		return outMsg;
	}
	
	public static SlRdDataMsg splitSlRdDataMsg(byte[] message)
	{
		SlRdDataMsg outMsg = new SlRdDataMsg();
		
		outMsg.locoAddress = (toInt(message[9]) * 128) + (toInt(message[4]));
		outMsg.isAddressAlreadyInUse = ((toInt(message[3]) & 0x30) == 0x30);
		outMsg.slot = toInt(message[2]);
		
		return outMsg;
	}
	
	public static LocoSpdMsg splitLocoSpdMsg(byte[] message)
	{
		LocoSpdMsg outMsg = new LocoSpdMsg();
		
		outMsg.slot = toInt(message[1]);
		outMsg.speed = toInt(message[2]);
		
		return outMsg;
	}
	
	public static LocoSndMsg splitLocoSndMsg(byte[] message)
	{
		LocoSndMsg outMsg = new LocoSndMsg();
		
		outMsg.slot = toInt(message[1]);
		outMsg.F5 = OnOffType.Off;
		outMsg.F6 = OnOffType.Off;
		outMsg.mute = OnOffType.Off;
		
		if ((message[2] & kF5) == kF5)
			outMsg.F5 = OnOffType.On;
		if ((message[2] & kF6) == kF6)
			outMsg.F6 = OnOffType.On;
		if ((message[2] & kMuteOn) == kMuteOn)
			outMsg.mute = OnOffType.On;
		
		return outMsg;
	}
	
	public static LocoDirfMsg splitLocoDirfMsg(byte[] message)
	{
		LocoDirfMsg outMsg = new LocoDirfMsg();
		outMsg.slot = message[1];
		
		outMsg.direction = DirectionType.Forward;
		if ((message[2] & kBackward) == kBackward)
			outMsg.direction = DirectionType.Backward;
		
		outMsg.light = OnOffType.Off;
		if ((message[2] & kLightsOn) == kLightsOn)
			outMsg.light = OnOffType.On;
		
		outMsg.horn = OnOffType.Off;
		if ((message[2] & kHornOn) == kHornOn)
			outMsg.horn = OnOffType.On;
		
		outMsg.bell = OnOffType.Off;
		if ((message[2] & kBellOn) == kBellOn)
			outMsg.bell = OnOffType.On;
		
		return outMsg;
	}
	
	public static LocoAdrMsg splitLocoAdrMsg(byte[] message)
	{
		LocoAdrMsg outMsg = new LocoAdrMsg();
		outMsg.Address = (toInt(message[1]) * 128) + toInt(message[2]);
		return outMsg;
	}
	
	public static PutTrainStateMsg splitPutTrainStateMsg(byte[] msg)
	{
		PutTrainStateMsg outMsg = new PutTrainStateMsg();
		outMsg.slot = toInt(msg[2]);
		outMsg.state = toInt(msg[3]);
		return outMsg;
	}
	
	public static PutSwitchStateMsg splitPutSwitchStateMsg(byte[] msg)
	{
		PutSwitchStateMsg outMsg = new PutSwitchStateMsg();
		outMsg.switchId = toInt(msg[2]);
		outMsg.state = toInt(msg[3]);
		return outMsg;
	}
	
	public static DoLocoInitMsg splitDoLocoInitMsg(byte[] msg)
	{
		DoLocoInitMsg outMsg = new DoLocoInitMsg();
		List<Integer> sensors = new ArrayList<>();
		
		outMsg.locoAddress = convertBytesToNatural(msg[2], msg[3]);
		final int sensorCount = toInt(msg[4]);
		for (int i = 1; i <= sensorCount; i++)
			sensors.add(convertBytesToNatural(msg[4 + (2 * i) - 1], msg[4 + (2 * i)]));
		
		outMsg.sensors = sensors;
		return outMsg;
	}
	
	public static PutInitOutcomeMsg splitPutInitOutcomeMsg(byte[] msg)
	{
		PutInitOutcomeMsg outMsg = new PutInitOutcomeMsg();
		outMsg.physicalLocoAddress = convertBytesToNatural(msg[2], msg[3]);
		outMsg.physicalSlotNum = toInt(msg[4]);
		outMsg.virtualLocoAddress = convertBytesToNatural(msg[5], msg[6]);
		outMsg.virtualSlotNum = toInt(msg[7]);
		return outMsg;
	}
	
	public static PutReadLayoutResponseMsg splitPutReadLayoutResponseMsg(byte[] msg)
	{
		PutReadLayoutResponseMsg outMsg = new PutReadLayoutResponseMsg();
		outMsg.responseFlag = toInt(msg[2]);
		outMsg.code = convertBytesToNatural(msg[3], msg[4]);
		return outMsg;
	}
	
	public static PutTrainInformationMsg splitPutTrainInformationMsg(byte[] msg)
	{
		PutTrainInformationMsg outMsg = new PutTrainInformationMsg();
		outMsg.slot = toInt(msg[2]);
		outMsg.speed = toInt(msg[3]);
		outMsg.direction = toInt(msg[4]);
		outMsg.light = toInt(msg[5]);
		outMsg.bell = toInt(msg[6]);
		outMsg.horn = toInt(msg[7]);
		outMsg.mute = toInt(msg[8]);
		return outMsg;
	}
	
	private static BytePair convertNaturalToBytes(int value)
	{
		return new BytePair((byte) (value % 128), (byte) (value >> 7)); // Divide by 128
	}
	
	private static int convertBytesToNatural(byte b, byte c)
	{
		return (int) b + (((int) c) << 7); // Multiply by 128
	}
	
	private static void makeChecksumByte(byte[] message)
	{
		final int len = message.length;
		byte checksum = (byte) 0xFF;
		for (int i = 0; i < len - 1; i++)
			checksum ^= message[i];
		message[len - 1] = checksum;
	}
	
	private static int toInt(byte b)
	{
		return ((int) b & 0xFF);
	}
}
